add evernote agent and specs

Ben Cornelis 8 years ago
parent
commit
2fd8283473

+ 3 - 0
.env.example

@@ -108,6 +108,9 @@ DROPBOX_OAUTH_SECRET=
108 108
 WUNDERLIST_OAUTH_KEY=
109 109
 WUNDERLIST_OAUTH_SECRET=
110 110
 
111
+EVERNOTE_OAUTH_KEY=
112
+EVERNOTE_OAUTH_SECRET=
113
+
111 114
 #############################
112 115
 #  AWS and Mechanical Turk  #
113 116
 #############################

+ 4 - 0
Gemfile

@@ -39,6 +39,10 @@ gem 'omniauth-dropbox'
39 39
 # UserLocationAgent
40 40
 gem 'haversine'
41 41
 
42
+# EvernoteAgent
43
+gem 'omniauth-evernote'
44
+gem 'evernote_oauth'
45
+
42 46
 # Optional Services.
43 47
 gem 'omniauth-37signals'          # BasecampAgent
44 48
 gem 'omniauth-wunderlist', github: 'wunderlist/omniauth-wunderlist', ref: 'd0910d0396107b9302aa1bc50e74bb140990ccb8'

+ 10 - 0
Gemfile.lock

@@ -179,6 +179,10 @@ GEM
179 179
     ethon (0.7.1)
180 180
       ffi (>= 1.3.0)
181 181
     eventmachine (1.0.7)
182
+    evernote-thrift (1.25.1)
183
+    evernote_oauth (0.2.3)
184
+      evernote-thrift
185
+      oauth (>= 0.4.1)
182 186
     execjs (2.3.0)
183 187
     extlib (0.9.16)
184 188
     faraday (0.9.1)
@@ -318,6 +322,10 @@ GEM
318 322
       omniauth-oauth2 (~> 1.0)
319 323
     omniauth-dropbox (0.2.0)
320 324
       omniauth-oauth (~> 1.0)
325
+    omniauth-evernote (1.2.1)
326
+      evernote-thrift
327
+      multi_json (~> 1.0)
328
+      omniauth-oauth (~> 1.0)
321 329
     omniauth-oauth (1.0.1)
322 330
       oauth
323 331
       omniauth (~> 1.0)
@@ -543,6 +551,7 @@ DEPENDENCIES
543 551
   dotenv-rails (~> 2.0.1)
544 552
   dropbox-api
545 553
   em-http-request (~> 1.1.2)
554
+  evernote_oauth
546 555
   faraday (~> 0.9.0)
547 556
   faraday_middleware (>= 0.10.0)
548 557
   feed-normalizer
@@ -576,6 +585,7 @@ DEPENDENCIES
576 585
   omniauth
577 586
   omniauth-37signals
578 587
   omniauth-dropbox
588
+  omniauth-evernote
579 589
   omniauth-tumblr
580 590
   omniauth-twitter
581 591
   omniauth-wunderlist!

+ 48 - 0
app/concerns/evernote_concern.rb

@@ -0,0 +1,48 @@
1
+module EvernoteConcern
2
+  extend ActiveSupport::Concern
3
+
4
+  included do
5
+    include Oauthable
6
+
7
+    validate :validate_evernote_options
8
+
9
+    valid_oauth_providers :evernote
10
+
11
+    gem_dependency_check { defined?(EvernoteOAuth) && Devise.omniauth_providers.include?(:evernote) }
12
+  end
13
+
14
+  def evernote_client
15
+    EvernoteOAuth::Client.new(
16
+      token:           evernote_oauth_token,
17
+      consumer_key:    evernote_consumer_key,
18
+      consumer_secret: evernote_consumer_secret,
19
+      sandbox: false
20
+    )
21
+  end
22
+
23
+  private
24
+
25
+  def validate_evernote_options
26
+    unless evernote_consumer_key.present? &&
27
+      evernote_consumer_secret.present? &&
28
+      evernote_oauth_token.present?
29
+      errors.add(:base, "Evernote consumer_key, consumer_secret, oauth_token, and oauth_token_secret are required to authenticate with the Twitter API.  You can provide these as options to this Agent, or as Credentials with the same names, but starting with 'evernote_'.")
30
+    end
31
+  end
32
+
33
+  def evernote_consumer_key
34
+    (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_key
35
+  end
36
+
37
+  def evernote_consumer_secret
38
+    (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_secret
39
+  end
40
+
41
+  def evernote_oauth_token
42
+    service && service.token
43
+  end
44
+
45
+  def evernote_oauth_token_secret
46
+    service && service.secret
47
+  end
48
+end

+ 373 - 0
app/models/agents/evernote_agent.rb

@@ -0,0 +1,373 @@
1
+module Agents
2
+  class EvernoteAgent < Agent
3
+    include EvernoteConcern
4
+
5
+    description <<-MD
6
+      The Evernote Agent connects with a user's Evernote note store.
7
+
8
+      To be able to use this agent with your account you need to authenticate with Evernote in the [Services](/services) section.
9
+
10
+      Options:
11
+
12
+        * `mode` - Two possible values:
13
+
14
+            - `update` Based on events it receives, the agent will create notes
15
+                       or update notes with the same `title` and `notebook`
16
+
17
+            - `read`   On a schedule, it will generate events containing data for newly
18
+                       added or updated notes
19
+
20
+        * `include_xhtml_content` - Set to `true` to include the content in ENML (Evernote Markup Language) of the note
21
+
22
+        * `note`
23
+
24
+          - When `mode` is `update` the parameters of `note` are the attributes of the note to be added/edited.
25
+            To edit a note, both `title` and `notebook` must be set.
26
+
27
+            For example, to add the tags 'comic' and 'CS' to a note titled 'xkcd Survey' in the notebook 'xkcd', use:
28
+
29
+                "notes": {
30
+                  "title": "xkcd Survey",
31
+                  "content": "",
32
+                  "notebook": "xkcd",
33
+                  "tagNames": "comic, CS"
34
+                }
35
+
36
+            If a note with the above title and notebook did note exist already, one would be created.
37
+
38
+          - When `mode` is `read` the values are search parameters.
39
+            Note: The `content` parameter is not used for searching.
40
+
41
+            For example, to find all notes with tag 'CS' in the notebook 'xkcd', use:
42
+
43
+                "notes": {
44
+                  "title": "",
45
+                  "content": "",
46
+                  "notebook": "xkcd",
47
+                  "tagNames": "CS"
48
+                }
49
+    MD
50
+
51
+    event_description <<-MD
52
+      When `mode` is `update`, events look like:
53
+
54
+          {
55
+            "title": "...",
56
+            "content": "...",
57
+            "notebook": "...",
58
+            "tags": "...",
59
+            "source": "...",
60
+            "sourceURL": "..."
61
+          }
62
+
63
+      When `mode` is `read`, events look like:
64
+
65
+          {
66
+            "title": "...",
67
+            "content": "...",
68
+            "notebook": "...",
69
+            "tags": "...",
70
+            "source": "...",
71
+            "sourceURL": "...",
72
+            "resources" : [
73
+              {
74
+                "url": "resource1_url",
75
+                "name": "resource1_name",
76
+                "mime_type": "resource1_mime_type"
77
+              }
78
+              ...
79
+            ]
80
+          }
81
+    MD
82
+
83
+    default_schedule "never"
84
+
85
+    def working?
86
+      event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
87
+    end
88
+
89
+    def default_options
90
+      {
91
+        "expected_update_period_in_days" => "2",
92
+        "mode" => "update",
93
+        "include_xhtml_content" => "false",
94
+        "note" => {
95
+          "title" => "{{title}}",
96
+          "content" => "{{content}}",
97
+          "notebook" => "{{notebook}}",
98
+          "tagNames" => "{{tag1}}, {{tag2}}"
99
+        }
100
+      }
101
+    end
102
+
103
+    def validate_options
104
+      errors.add(:base, "mode must be 'update' or 'read'") unless %w(read update).include?(options[:mode])
105
+
106
+      if options[:mode] == "update" && schedule != "never"
107
+        errors.add(:base, "when mode is set to 'update', schedule must be 'never'")
108
+      end
109
+
110
+      if options[:mode] == "read" && schedule == "never"
111
+        errors.add(:base, "when mode is set to 'read', agent must have a schedule")
112
+      end
113
+
114
+      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
115
+
116
+      if options[:mode] == "update" && options[:note].values.all?(&:empty?)
117
+        errors.add(:base, "you must specify at least one note parameter to create or update a note")
118
+      end
119
+    end
120
+
121
+    def include_xhtml_content?
122
+      options[:include_xhtml_content] == "true"
123
+    end
124
+
125
+    def receive(incoming_events)
126
+      if options[:mode] == "update"
127
+        incoming_events.each do |event|
128
+          note = note_store.create_or_update_note(note_params(event))
129
+          create_event :payload => note.attr(include_content: include_xhtml_content?)
130
+        end
131
+      end
132
+    end
133
+
134
+    def check
135
+      if options[:mode] == "read"
136
+        opts = note_params(options)
137
+
138
+        # convert time to evernote timestamp format:
139
+        # https://dev.evernote.com/doc/reference/Types.html#Typedef_Timestamp
140
+        opts.merge!(agent_created_at: created_at.to_i * 1000)
141
+        opts.merge!(last_checked_at: (memory[:last_checked_at] ||= created_at.to_i * 1000))
142
+
143
+        if opts[:tagNames]
144
+          opts.merge!(notes_with_tags: (memory[:notes_with_tags] ||=
145
+            NoteStore::Search.new(note_store, {tagNames: opts[:tagNames]}).note_guids))
146
+        end
147
+
148
+        notes = NoteStore::Search.new(note_store, opts).notes
149
+        notes.each do |note|
150
+          memory[:notes_with_tags] << note.guid unless memory[:notes_with_tags].include?(note.guid)
151
+
152
+          create_event :payload => note.attr(include_resources: true, include_content: include_xhtml_content?)
153
+        end
154
+
155
+        memory[:last_checked_at] = Time.now.to_i * 1000
156
+        save!
157
+      end
158
+    end
159
+
160
+    private
161
+
162
+    def note_params(options)
163
+      params = interpolated(options)[:note]
164
+      errors.add(:base, "only one notebook allowed") unless params[:notebook].to_s.split(", ") == 1
165
+      params[:tagNames] = params[:tagNames].to_s.split(", ")
166
+      params
167
+    end
168
+
169
+    def evernote_note_store
170
+      evernote_client.note_store
171
+    end
172
+
173
+    def note_store
174
+      NoteStore.new(evernote_note_store)
175
+    end
176
+
177
+    # wrapper for evernote api NoteStore
178
+    # https://dev.evernote.com/doc/reference/
179
+    class NoteStore
180
+      attr_reader :en_note_store
181
+      delegate :createNote, :updateNote, :getNote, :listNotebooks, :listTags, :getNotebook,
182
+               :createNotebook, :findNotesMetadata, :getNoteTagNames, :to => :en_note_store
183
+
184
+      def initialize(en_note_store)
185
+        @en_note_store = en_note_store
186
+      end
187
+
188
+      def create_or_update_note(params)
189
+        search = Search.new(self, {title: params[:title], notebook: params[:notebook]})
190
+        # evernote search can only filter notes with titles containing a substring;
191
+        # this finds a note with the exact title
192
+        note = search.notes.detect {|note| note.title == params[:title]}
193
+        if note
194
+          # a note with specified title and notebook exists, so update it
195
+          update_note(params.merge(guid: note.guid, notebookGuid: note.notebookGuid))
196
+        else
197
+          # create the notebook unless it already exists
198
+          notebook = find_notebook(name: params[:notebook])
199
+          notebook_guid =
200
+            notebook ? notebook.guid : create_notebook(params[:notebook]).guid
201
+
202
+          create_note(params.merge(notebookGuid: notebook_guid))
203
+        end
204
+      end
205
+
206
+      def create_note(params)
207
+        note = Evernote::EDAM::Type::Note.new(with_wrapped_content(params))
208
+        en_note = createNote(note)
209
+        find_note(en_note.guid)
210
+      end
211
+
212
+      def update_note(params)
213
+        # do not empty note properties that have not been set in `params`
214
+        params.keys.each { |key| params.delete(key) unless params[key].present? }
215
+        params = with_wrapped_content(params)
216
+
217
+        # append specified tags instead of replacing current tags
218
+        tags = getNoteTagNames(params[:guid])
219
+        tags.each { |tag|
220
+          params[:tagNames] << tag unless params[:tagNames].include?(tag) }
221
+
222
+        note = Evernote::EDAM::Type::Note.new(params)
223
+        updateNote(note)
224
+        find_note(params[:guid])
225
+      end
226
+
227
+      def find_note(guid)
228
+        # https://dev.evernote.com/doc/reference/NoteStore.html#Fn_NoteStore_getNote
229
+        en_note = getNote(guid, true, false, false, false)
230
+        build_note(en_note)
231
+      end
232
+
233
+      def build_note(en_note)
234
+        notebook = find_notebook(guid: en_note.notebookGuid).name
235
+        tags = en_note.tagNames || find_tags(en_note.tagGuids.to_a).map(&:name)
236
+        Note.new(en_note, notebook, tags)
237
+      end
238
+
239
+      def find_tags(guids)
240
+        listTags.select {|tag| guids.include?(tag.guid)}
241
+      end
242
+
243
+      def find_notebook(params)
244
+        if params[:guid]
245
+          listNotebooks.detect {|notebook| notebook.guid == params[:guid]}
246
+        elsif params[:name]
247
+          listNotebooks.detect {|notebook| notebook.name == params[:name]}
248
+        end
249
+      end
250
+
251
+      def create_notebook(name)
252
+        notebook = Evernote::EDAM::Type::Notebook.new(name: name)
253
+        createNotebook(notebook)
254
+      end
255
+
256
+      def with_wrapped_content(params)
257
+        params.delete(:notebook)
258
+
259
+        if params[:content]
260
+          params[:content] =
261
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
262
+            "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \
263
+            "<en-note>#{params[:content]}</en-note>"
264
+        end
265
+
266
+        params
267
+      end
268
+
269
+      class Search
270
+        attr_reader :note_store, :opts
271
+        def initialize(note_store, opts)
272
+          @note_store = note_store
273
+          @opts = opts
274
+        end
275
+
276
+        def filtered_metadata
277
+          filter, spec = create_filter, create_spec
278
+          metadata = note_store.findNotesMetadata(filter, 0, 100, spec).notes
279
+        end
280
+
281
+        def note_guids
282
+          filtered_metadata.map(&:guid)
283
+        end
284
+
285
+        def notes
286
+          metadata = filtered_metadata
287
+
288
+          if opts[:last_checked_at] && opts[:tagNames]
289
+
290
+            # evernote does note change Note#updated timestamp when a tag is added to a note
291
+            # the following selects recently updated notes
292
+            # and notes that recently had the specified tags added
293
+            metadata.select! do |note_data|
294
+              note_data.updated > opts[:last_checked_at] ||
295
+              (!opts[:notes_with_tags].include?(note_data.guid) && note_data.created > opts[:agent_created_at])
296
+            end
297
+
298
+          elsif opts[:last_checked_at]
299
+            metadata.select! { |note_data| note_data.updated > opts[:last_checked_at] }
300
+          end
301
+
302
+          metadata.map! { |note_data| note_store.find_note(note_data.guid) }
303
+          metadata
304
+        end
305
+
306
+        private
307
+
308
+        def create_filter
309
+          filter = Evernote::EDAM::NoteStore::NoteFilter.new
310
+
311
+          # evernote search grammar:
312
+          # https://dev.evernote.com/doc/articles/search_grammar.php#Search_Terms
313
+          query_terms = []
314
+          query_terms << "notebook:\"#{opts[:notebook]}\""   if opts[:notebook].present?
315
+          query_terms << "intitle:\"#{opts[:title]}\""       if opts[:title].present?
316
+          query_terms << "updated:day-1"                     if opts[:last_checked_at].present?
317
+          opts[:tagNames].to_a.each { |tag| query_terms << "tag:#{tag}" }
318
+
319
+          filter.words = query_terms.join(" ")
320
+          filter
321
+        end
322
+
323
+        def create_spec
324
+          Evernote::EDAM::NoteStore::NotesMetadataResultSpec.new(
325
+            includeTitle: true,
326
+            includeAttributes: true,
327
+            includeNotebookGuid: true,
328
+            includeTagGuids: true,
329
+            includeUpdated: true,
330
+            includeCreated: true
331
+          )
332
+        end
333
+      end
334
+    end
335
+
336
+    class Note
337
+      attr_accessor :en_note
338
+      attr_reader :notebook, :tags
339
+      delegate :guid, :notebookGuid, :title, :tagGuids, :content, :resources,
340
+               :attributes, :to => :en_note
341
+
342
+      def initialize(en_note, notebook, tags)
343
+        @en_note = en_note
344
+        @notebook = notebook
345
+        @tags = tags
346
+      end
347
+
348
+      def attr(opts = {})
349
+        return_attr = {
350
+          title:        title,
351
+          notebook:     notebook,
352
+          tags:         tags,
353
+          source:       attributes.source,
354
+          source_url:   attributes.sourceURL
355
+        }
356
+
357
+        return_attr[:content] = content if opts[:include_content]
358
+
359
+        if opts[:include_resources] && resources
360
+          return_attr[:resources] = []
361
+          resources.each do |resource|
362
+            return_attr[:resources] << {
363
+              url:       resource.attributes.sourceURL,
364
+              name:      resource.attributes.fileName,
365
+              mime_type: resource.mime
366
+            }
367
+          end
368
+        end
369
+        return_attr
370
+      end
371
+    end
372
+  end
373
+end

+ 10 - 0
config/initializers/devise.rb

@@ -263,6 +263,16 @@ Devise.setup do |config|
263 263
     config.omniauth :wunderlist, key, secret
264 264
   end
265 265
 
266
+  if defined?(OmniAuth::Strategies::Evernote) &&
267
+    (key = ENV["EVERNOTE_OAUTH_KEY"]).present? &&
268
+    (secret = ENV["EVERNOTE_OAUTH_SECRET"]).present?
269
+    # for production:
270
+    config.omniauth :evernote, key, secret
271
+
272
+    # for development:
273
+    # config.omniauth :evernote, key, secret, client_options: { :site => 'https://sandbox.evernote.com' }
274
+  end
275
+
266 276
   # ==> Warden configuration
267 277
   # If you want to use other strategies, that are not supported by Devise, or
268 278
   # change the failure app, you can configure them inside the config.warden block.

+ 1 - 0
config/locales/devise.en.yml

@@ -33,6 +33,7 @@ en:
33 33
       37signals: "37Signals (Basecamp)"
34 34
       dropbox: "Dropbox"
35 35
       wunderlist: 'Wunderlist'
36
+      evernote: "Evernote"
36 37
     passwords:
37 38
       no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
38 39
       send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."

+ 297 - 0
spec/models/agents/evernote_agent_spec.rb

@@ -0,0 +1,297 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::EvernoteAgent do
4
+
5
+  let(:note_store) do
6
+    class FakeEvernoteNoteStore
7
+      attr_accessor :notes, :tags, :notebooks
8
+      def initialize
9
+        @notes, @tags, @notebooks = [], [], []
10
+      end
11
+
12
+      def createNote(note)
13
+        note.attributes = OpenStruct.new(source: nil, sourceURL: nil)
14
+        note.guid = @notes.length + 1
15
+        @notes << note
16
+        note
17
+      end
18
+
19
+      def updateNote(note)
20
+        note.attributes = OpenStruct.new(source: nil, sourceURL: nil)
21
+        old_note = @notes.find {|en_note| en_note.guid == note.guid}
22
+        @notes[@notes.index(old_note)] = note
23
+        note
24
+      end
25
+
26
+      def getNote(guid, *other_args)
27
+        @notes.find {|note| note.guid == guid}
28
+      end
29
+
30
+      def createNotebook(notebook)
31
+        notebook.guid = @notebooks.length + 1
32
+        @notebooks << notebook
33
+        notebook
34
+      end
35
+
36
+      def createTag(tag)
37
+        tag.guid = @tags.length + 1
38
+        @tags << tag
39
+        tag
40
+      end
41
+
42
+      def listNotebooks; @notebooks; end
43
+
44
+      def listTags; @tags; end
45
+
46
+      def getNoteTagNames(guid)
47
+        getNote(guid).try(:tagNames) || []
48
+      end
49
+
50
+      def findNotesMetadata(*args); end
51
+    end
52
+
53
+    note_store = FakeEvernoteNoteStore.new
54
+    stub.any_instance_of(Agents::EvernoteAgent).evernote_note_store { note_store }
55
+    note_store
56
+  end
57
+
58
+  describe "#receive" do
59
+    context "when mode is set to 'update'" do
60
+      before do
61
+        @options = {
62
+          :mode => "update",
63
+          :include_xhtml_content => "false",
64
+          :expected_update_period_in_days => "2",
65
+          :note => {
66
+            :title     => "{{title}}",
67
+            :content   => "{{content}}",
68
+            :notebook  => "{{notebook}}",
69
+            :tagNames  => "{{tag1}}, {{tag2}}"
70
+          }
71
+        }
72
+        @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options)
73
+        @agent.service = services(:generic)
74
+        @agent.user = users(:bob)
75
+        @agent.save!
76
+
77
+        @event = Event.new
78
+        @event.agent = agents(:bob_website_agent)
79
+        @event.payload = { :title => "xkcd Survey",
80
+                           :content => "The xkcd Survey: Big Data for a Big Planet",
81
+                           :notebook => "xkcd",
82
+                           :tag1 => "funny",
83
+                           :tag2 => "data" }
84
+        @event.save!
85
+
86
+        tag1 = OpenStruct.new(name: "funny")
87
+        tag2 = OpenStruct.new(name: "data")
88
+        [tag1, tag2].each { |tag| note_store.createTag(tag) }
89
+      end
90
+
91
+      it "adds a note for any payload it receives" do
92
+        stub(note_store).findNotesMetadata { OpenStruct.new(notes: []) }
93
+        Agents::EvernoteAgent.async_receive(@agent.id, [@event.id])
94
+
95
+        expect(note_store.notes.size).to eq(1)
96
+        expect(note_store.notes.first.title).to eq("xkcd Survey")
97
+        expect(note_store.notebooks.size).to eq(1)
98
+        expect(note_store.tags.size).to eq(2)
99
+        
100
+        expect(@agent.events.count).to eq(1)
101
+        expect(@agent.events.first.payload).to eq({
102
+          "title" => "xkcd Survey",
103
+          "notebook" => "xkcd",
104
+          "tags" => ["funny", "data"],
105
+          "source" => nil,
106
+          "source_url" => nil
107
+        })
108
+      end
109
+
110
+      context "a note with the same title and notebook exists" do
111
+        before do
112
+          note1 = OpenStruct.new(title: "xkcd Survey", notebookGuid: 1)
113
+          note2 = OpenStruct.new(title: "Footprints", notebookGuid: 1)
114
+          [note1, note2].each { |note| note_store.createNote(note) }
115
+          note_store.createNotebook(OpenStruct.new(name: "xkcd"))
116
+
117
+          stub(note_store).findNotesMetadata {
118
+            OpenStruct.new(notes: [note1]) }
119
+        end
120
+
121
+        it "updates the existing note" do
122
+          Agents::EvernoteAgent.async_receive(@agent.id, [@event.id])
123
+
124
+          expect(note_store.notes.size).to eq(2)
125
+          expect(note_store.getNote(1).tagNames).to eq(["funny", "data"])
126
+          expect(@agent.events.count).to eq(1)
127
+        end
128
+      end
129
+
130
+      context "include_xhtml_content is set to 'true'" do
131
+        before do
132
+          @agent.options[:include_xhtml_content] = "true"
133
+          @agent.save!
134
+        end
135
+
136
+        it "creates an event with note content wrapped in ENML" do
137
+          stub(note_store).findNotesMetadata { OpenStruct.new(notes: []) }
138
+          Agents::EvernoteAgent.async_receive(@agent.id, [@event.id])
139
+
140
+          payload = @agent.events.first.payload
141
+
142
+          expect(payload[:content]).to eq(
143
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
144
+            "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \
145
+            "<en-note>The xkcd Survey: Big Data for a Big Planet</en-note>"
146
+          )
147
+        end
148
+      end
149
+    end
150
+  end
151
+
152
+  describe "#check" do
153
+    context "when mode is set to 'read'" do
154
+      before do
155
+        @options = {
156
+          :mode => "read",
157
+          :include_xhtml_content => "false",
158
+          :expected_update_period_in_days => "2",
159
+          :note => {
160
+            :title     => "",
161
+            :content   => "",
162
+            :notebook  => "xkcd",
163
+            :tagNames  => "funny, comic"
164
+          }
165
+        }
166
+        @checker = Agents::EvernoteAgent.new(:name => "evernote reader", :options => @options)
167
+
168
+        @checker.service = services(:generic)
169
+        @checker.user = users(:bob)
170
+        @checker.schedule = "every_2h"
171
+
172
+        @checker.save!
173
+        @checker.created_at = 1.minute.ago
174
+
175
+        note_store.createNote(
176
+          OpenStruct.new(title: "xkcd Survey",
177
+                         notebookGuid: 1,
178
+                         updated: 2.minutes.ago.to_i * 1000,
179
+                         tagNames: ["funny", "comic"])
180
+        )
181
+        note_store.createNotebook(OpenStruct.new(name: "xkcd"))
182
+        tag1 = OpenStruct.new(name: "funny")
183
+        tag2 = OpenStruct.new(name: "comic")
184
+        [tag1, tag2].each { |tag| note_store.createTag(tag) }
185
+
186
+        stub(note_store).findNotesMetadata {
187
+          notes = note_store.notes.select do |note|
188
+            note.notebookGuid == 1 &&
189
+            %w(funny comic).all? { |tag_name| note.tagNames.include?(tag_name) }
190
+          end
191
+          OpenStruct.new(notes: notes)
192
+        }
193
+      end
194
+
195
+      context "the first time it checks" do
196
+        it "returns only notes created/updated since it was created" do
197
+          expect { @checker.check }.to change { Event.count }.by(0)
198
+        end
199
+      end
200
+
201
+      context "on subsequent checks" do
202
+        it "returns notes created/updated since the last time it checked" do
203
+          expect { @checker.check }.to change { Event.count }.by(0)
204
+
205
+          future_time = (Time.now + 1.minute).to_i * 1000
206
+          note_store.createNote(
207
+            OpenStruct.new(title: "Footprints",
208
+                           notebookGuid: 1,
209
+                           tagNames: ["funny", "comic", "recent"],
210
+                           updated: future_time))
211
+
212
+          note_store.createNote(
213
+            OpenStruct.new(title: "something else",
214
+                           notebookGuid: 2,
215
+                           tagNames: ["funny", "comic"],
216
+                           updated: future_time))
217
+
218
+          expect { @checker.check }.to change { Event.count }.by(1)
219
+        end
220
+
221
+        it "returns notes tagged since the last time it checked" do
222
+          note_store.createNote(
223
+            OpenStruct.new(title: "Footprints",
224
+                           notebookGuid: 1,
225
+                           tagNames: [],
226
+                           created: Time.now.to_i * 1000,
227
+                           updated: Time.now.to_i * 1000))
228
+          @checker.check
229
+
230
+          note_store.getNote(2).tagNames = ["funny", "comic"]
231
+
232
+          expect { @checker.check }.to change { Event.count }.by(1)
233
+        end
234
+      end
235
+    end
236
+  end
237
+
238
+  describe "#validation" do
239
+    before do
240
+      @options = {
241
+        :mode => "update",
242
+        :include_xhtml_content => "false",
243
+        :expected_update_period_in_days => "2",
244
+        :note => {
245
+          :title     => "{{title}}",
246
+          :content   => "{{content}}",
247
+          :notebook  => "{{notebook}}",
248
+          :tagNames  => "{{tag1}}, {{tag2}}"
249
+        }
250
+      }
251
+      @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options)
252
+      @agent.service = services(:generic)
253
+      @agent.user = users(:bob)
254
+      @agent.save!
255
+
256
+      expect(@agent).to be_valid
257
+    end
258
+
259
+    it "requires the mode to be 'update' or 'read'" do
260
+      @agent.options[:mode] = ""
261
+      expect(@agent).not_to be_valid
262
+    end
263
+
264
+    context "mode is set to 'update'" do
265
+      before do
266
+        @agent.options[:mode] = "update"
267
+      end
268
+
269
+      it "requires some note parameter to be present" do
270
+        @agent.options[:note].keys.each { |k| @agent.options[:note][k] = "" }
271
+        expect(@agent).not_to be_valid
272
+      end
273
+
274
+      it "requires schedule to be 'never'" do
275
+        @agent.schedule = 'never'
276
+        expect(@agent).to be_valid
277
+
278
+        @agent.schedule = 'every_1m'
279
+        expect(@agent).not_to be_valid
280
+      end
281
+    end
282
+
283
+    context "mode is set to 'read'" do
284
+      before do
285
+        @agent.options[:mode] = "read"
286
+      end
287
+
288
+      it "requires a schedule to be set" do
289
+        @agent.schedule = 'every_1m'
290
+        expect(@agent).to be_valid
291
+
292
+        @agent.schedule = 'never'
293
+        expect(@agent).not_to be_valid
294
+      end
295
+    end
296
+  end
297
+end